/*=============================================================================
*
*   Copyright (C) 2020 All rights reserved.
*
*   Filename:		point.hpp
*
*   Author: Wang Zhecheng - wangzhecheng@yeah.net
*
*   Date: 2020-01-17 11:27
*
*   Last Editors: Wang Zhecheng - wangzhecheng@yeah.net
*
*   Last modified:	2020-09-02 23:41
*
*   Description:
*
=============================================================================*/
#ifndef BASIC_POINT_CLASS_H
#define BASIC_POINT_CLASS_H

#include "../../arithmetic/sorting.hpp"
#include "../../io/ploter.hpp"
#include "../../util/indexed.hpp"
#include "../../util/register_macro.hpp"
#include "../basic_geometry.hpp"
#include "point_impl.hpp"
#include <boost/geometry.hpp>
#include <boost/property_tree/ptree.hpp>
#include <cmath>
#include <memory>

namespace geoxfem {
namespace geometry {

template <size_t dim>
struct Point : util::Indexed {
  using ptree = boost::property_tree::ptree;
  using util::Indexed::operator>;
  using util::Indexed::operator<;

  /* constructors and destructor{{{*/

  /**
   * @brief Point default constructor.
   */
  Point()
      : util::Indexed{}, m_pImpl{new point_impl<dim>()}, m_allocated{true} {}
  /**
   * @brief copy constructor. (shallow copy)
   *
   * @param rhs
   */
  Point(const Point& rhs) : util::Indexed{rhs}, m_allocated{rhs.m_allocated} {
    if(rhs.m_allocated)
      m_pImpl = new point_impl<dim>(*rhs.m_pImpl);
    else
      m_pImpl = rhs.m_pImpl;
  }
  /**
   * @brief move constructor.
   *
   * @param rhs
   */
  Point(const Point&& rhs)
      : util::Indexed{std::move(rhs)}, m_allocated{std::move(rhs.m_allocated)} {
    if(rhs.m_allocated)
      m_pImpl = new point_impl<dim>(*rhs.m_pImpl);
    else
      m_pImpl = rhs.m_pImpl;
  }
  /**
   * @brief ~Point
   */
  virtual ~Point() {
    if(m_allocated)
      delete m_pImpl;
  }
  /**
   * @brief copy assignment
   *
   * @param _pt
   *
   * @return
   */
  const Point& operator=(const Point& rhs) {
    if(&rhs == this)
      return *this;
    setIndex(rhs.getIndex());
    m_allocated = rhs.m_allocated;
    if(rhs.m_allocated)
      m_pImpl = new point_impl<dim>(*rhs.m_pImpl);
    else
      m_pImpl = rhs.m_pImpl;

    return *this;
  }
  Point& operator=(const Point&& rhs) {
    if(&rhs == this)
      return *this;
    setIndex(rhs.getIndex());
    m_allocated = rhs.m_allocated;
    m_pImpl     = new point_impl<dim>(*rhs.m_pImpl);
    return *this;
  }
  /**
   * @brief move assignment
   *
   * @param rhs
   *
   * @return
   */
  Point& operator=(Point&& rhs) {
    if(&rhs == this)
      return *this;
    setIndex(std::move(rhs.getIndex()));
    m_allocated = std::move(rhs.m_allocated);
    if(m_allocated)
      m_pImpl = new point_impl<dim>(*rhs.m_pImpl);
    else
      m_pImpl = std::move(rhs.m_pImpl);

    return *this;
  }
  /**
   * @brief Point
   *
   * @param p
   * @param id
   * @param allocated
   */
  Point(point_impl<dim>* p, const int& id, const bool& allocated)
      : util::Indexed{id}, m_pImpl{p}, m_allocated{allocated} {}
  /**
   * @brief Point constructor with three dimensions and index
   *
   * @param
   * @param
   * @param
   * @param
   */
  Point(const _real&, const _real&, const _real& = 0, const int& = -1) {
    GEOXFEM_NOT_IMPLEMENTED();
  }
  /**
   * @brief construcctor with ptree
   *
   * @param ptree
   */
  Point(const ptree&) {
    GEOXFEM_NOT_IMPLEMENTED();
  }
  /**
   * @brief Point
   *
   * @param _coord coordinates in Eigen Vector form
   * @param id point index
   */
  Point(const Eigen::Matrix<_real, dim, 1>& _coord, const int& id = -1)
      : util::Indexed{id},
        m_pImpl{new point_impl<dim>(_coord)},
        m_allocated{true} {}
  /*}}}*/
  /*miscs{{{*/

  /**
   * @brief shallowClone
   *
   * @return
   */
  Point shallowClone() const {
    return Point(m_pImpl, getIndex(), false);
  }
  /**
   * @brief deepClone
   *
   * @return
   */
  Point deepClone() const {
    return Point(new point_impl<dim>(*m_pImpl), getIndex(), true);
  }
  /**
   * @brief operator==
   *
   * @param rhs target point
   *
   * @return if equals
   */
  bool operator==(const Point& rhs) const {
    return getIndex() == rhs.getIndex() && *m_pImpl == *rhs.m_pImpl;
  }
  /**
   * @brief operator<< >
   *
   * @param os
   * @param p
   *
   * @return
   */
  friend std::ostream& operator<<(std::ostream& os, const Point<dim>& p) {
    os << "Point" << dim << "D #" << p.getIndex() << "(" << p.x();
    for(size_t i = 1; i != dim; ++i)
      os << ", " << p.getCoord(i);
    os << ")";
    return os;
  }

  /**
   * @brief plot
   *
   * @param plt
   * @param pt_cfg
   */
  virtual void plot(io::DataPloter&    plt,
                    const std::string& pt_cfg = "\0") const {
    plt.plotPoint(x(), y(), z(), pt_cfg);
  }
  /*}}}*/
  /*coordinate access{{{*/

  /** return coords array
   *
   * @return
   */
  inline Eigen::Matrix<_real, dim, 1> getCoord() const {
    return m_pImpl->m_coord;
  }
  /**
   * @brief get #n coordinate
   *
   * @param n
   *
   * @return
   */
  inline _real getCoord(const size_t& n) const {
    return n < dim ? m_pImpl->m_coord[Eigen::Index(n)] : 0;
  }
  /**
   * @brief set #n coordinate
   *
   * @param n
   * @param _value
   */
  inline void setCoord(const size_t& n, const _real& value) {
    if(n < dim)
      m_pImpl->m_coord[Eigen::Index(n)] = value;
  }
  inline void setCoord(const VectorXR& value) {
    assert(value.size() == dim);
    m_pImpl->m_coord = value;
  }
  /**
   * @brief x
   *
   * @return
   */
  inline _real x() const {
    return getCoord(0);
  }
  /**
   * @brief y
   *
   * @return
   */
  inline _real y() const {
    return getCoord(1);
  }
  /**
   * @brief z
   *
   * @return
   */
  inline _real z() const {
    return getCoord(2);
  }
  /**
   * @brief setX
   *
   * @param _value
   */
  inline void setX(const _real& _value) {
    setCoord(0, _value);
  }
  /**
   * @brief setY
   *
   * @param _value
   */
  inline void setY(const _real& _value) {
    setCoord(1, _value);
  }
  /**
   * @brief setZ
   *
   * @param _value
   */
  inline void setZ(const _real& _value) {
    setCoord(2, _value);
  }
  /*}}}*/
  /*arithmetic{{{*/

  Point<dim> operator+(const Point<dim>& rhs) const {
    return Point<dim>(getCoord() + rhs.getCoord());
  }
  Point<dim> operator-(const Point<dim>& rhs) const {
    return Point<dim>(getCoord() - rhs.getCoord());
  }
  template <typename T>
  Point<dim> operator*(const T& rhs) const {
    return Point<dim>(getCoord() * rhs);
  }
  template <typename T>
  Point<dim> operator/(const T& rhs) const {
    return Point<dim>(getCoord() / rhs);
  }
  Point<dim>& operator+=(const Point<dim>& rhs) {
    this->getCoord() += rhs.getCoord();
    return *this;
  }
  Point<dim>& operator-=(const Point<dim>& rhs) {
    this->getCoord() -= rhs.getCoord();
    return *this;
  }
  template <typename T>
  Point<dim>& operator*=(const T& rhs) {
    this->getCoord() *= rhs;
    return *this;
  }
  template <typename T>
  Point<dim>& operator/=(const T& rhs) {
    this->getCoord /= rhs;
    return *this;
  }

  /*}}}*/

private:
  point_impl<dim>* m_pImpl;
  bool             m_allocated;
};

/*partial specialization{{{*/
template <>
Point<2>::Point(const _real& _x, const _real& _y, const _real&, const int& id)
    : util::Indexed{id},
      m_pImpl{new point_impl<2>(_x, _y)},
      m_allocated(true) {}
template <>
Point<3>::Point(const _real& _x,
                const _real& _y,
                const _real& _z,
                const int&   id)
    : util::Indexed{id},
      m_pImpl{new point_impl<3>(_x, _y, _z)},
      m_allocated(true) {}
template <>
Point<2>::Point(const ptree& data) : m_allocated{true} {
  const _real x  = data.get<_real>("x0");
  const _real y  = data.get<_real>("x1");
  int         id = data.get<int>("id");
  m_pImpl        = new point_impl<2>(x, y);
  setIndex(id);
}
template <>
Point<3>::Point(const ptree& data) : m_allocated{true} {
  const _real x  = data.get<_real>("x0");
  const _real y  = data.get<_real>("x1");
  const _real z  = data.get<_real>("x2");
  int         id = data.get<int>("id");
  m_pImpl        = new point_impl<3>(x, y, z);
  setIndex(id);
}
/*}}}*/

using Point2D = Point<2> ;
using Point3D = Point<3> ;

}  // namespace geometry

namespace traits {
template <size_t dim>
struct dimension<geometry::Point<dim>> {
  static constexpr size_t value = dim;
};
template <typename T>
struct point_type {
  using type = typename geometry::Point<dimension<T>::value>;
};
}  // namespace traits

/*point sort algorithm{{{*/
namespace algorithm {
template <size_t dim>
struct clock_wise<geometry::Point<dim>> {
  using point_type = geometry::Point<dim>;
  point_type m_centre;
  point_type m_start;
  clock_wise(const point_type& centre) : m_centre(centre) {
    point_type ps{centre};
    ps.setCoord(ps.x() + 1, 0);
  }
  clock_wise(const point_type& centre, const point_type& start)
      : m_centre(centre), m_start(start) {}
  clock_wise(const std::vector<point_type>& geometry)
      : m_centre{geometry::getCentroid(geometry)}, m_start{geometry.front()} {}
  bool operator()(const point_type& p1, const point_type& p2) const {
    const _real th1 =
      geometry::anti_clock_angle<point_type>(m_start, p1, m_centre)
        .template rad<geometry::zero_to_2pi>();
    const _real th2 =
      geometry::anti_clock_angle<point_type>(m_start, p2, m_centre)
        .template rad<geometry::zero_to_2pi>();
    return th1 < th2;
  }
  bool operator()(point_type* p1, point_type* p2) const {
    return operator()(*p1, *p2);
  }
};

/*}}}*/

}  // namespace algorithm
}  // namespace geoxfem

/* Custom hash function{{{*/
struct pointHasher {
  template <size_t dim>
  size_t operator()(const geoxfem::geometry::Point<dim>& point) const {
    return std::hash<int>{}(point.getIndex());
  }
  template <size_t dim>
  size_t operator()(const geoxfem::geometry::Point<dim>* pPoint) const {
    return std::hash<int>{}(pPoint->getIndex());
  }
  template <size_t dim>
  size_t operator()(
    const std::unique_ptr<geoxfem::geometry::Point<dim>>& pPoint) const {
    return std::hash<int>{}(pPoint->getIndex());
  }
};
/*}}}*/

template <typename Geometry, size_t dim>
struct boost::geometry::detail::append::
  append_point<Geometry, geoxfem::geometry::Point<dim>> {
  using Point = geoxfem::geometry::Point<dim>;
  static inline void
  apply(Geometry& geometry, Point const& point, int = 0, int = 0) {
    typename geometry::point_type<Geometry>::type copy;
    geometry::detail::conversion::convert_point_to_point(point, copy);
    copy.setIndex(point.getIndex());
    traits::push_back<Geometry>::apply(geometry, copy);
  }
};

BOOST_GEOMETRY_REGISTER_LINESTRING_TEMPLATED(std::vector);

GEOXFEM_REGISTER_2D_POINT(geoxfem::geometry::Point2D, _real, x, y, setX, setY);
GEOXFEM_REGISTER_3D_POINT(
  geoxfem::geometry::Point3D, _real, x, y, z, setX, setY, setZ);

#endif  /// BASIC_POINT_CLASS_H
